fix(mutmut): extract 30 @dataclass-with-methods classes to free functions (issue #282) - #303
Merged
Merged
Conversation
…ions (issue #282) mutmut categorically skips the body of any @dataclass-decorated ClassDef (mutmut/mutation/file_mutation.py:236), including @staticmethod members and __post_init__ dunders, so logic placed on methods carries zero mutation coverage regardless of test-loader naming. Issues #262/#280/#283 established the fix (extract methods to free functions, keep the dataclass pure data) for ConstraintSet, RepoBadge, and Ranking; this closes the systemic sweep across the remaining 30 classes the issue's own AST scan found (26 files, spanning mcp_server/core, mcp_server/handlers/consolidation, mcp_server/shared, fuzz/, and benchmarks/lib/). Every extracted function now attributes a real, scoped mutation tally (0 attributed -> a number, matching the RepoBadge precedent) — verified via per-file `mutmut run "<module>.x_<func>__mutmut_*"` isolation runs (a 26-file combined run gave unreliable results and was discarded; see the per-file tallies below). Surviving mutants were killed by strengthening assertions (precision-insensitive test fixtures, message-substring assertions matching the padded/typo'd input, missing negative-case coverage) or documented as equivalent at the use site. Also found and fixed one real regression risk while sweeping: a `getattr(goal, "is_active", False)` call site in recall_pipeline.py silently defaulted to False once the `is_active` property was removed, which would have disabled A3 goal-maintenance recall re-ranking with no signal — grep for `.attr` patterns alone does not catch getattr() defaults. Rebased onto a fast-moving origin/main mid-session (issue #276's headless_authoring.py split moved CycleBudget to a new cycle_types.py); the CycleBudget extraction was re-applied against the new file location. Closes #282
's sweep Verified per-file mutant tallies for all 26 files/61 extracted functions from the #282 sweep via isolated `mutmut run "<pattern>"` invocations (a combined 26-file run gave unreliable "survived"/"not checked" counts — discarded per this branch's own prior checkpoint). Two methodology bugs found while producing trustworthy numbers, both fixed at the source rather than worked around: - tests_py/fuzz/test_corpus_replay.py imported replay_corpus.py via importlib.util.spec_from_file_location under the bare name "replay_corpus", not the package-qualified "fuzz.replay_corpus" mutmut's trampoline dispatch expects -- every mutant in that file was invisible to mutation testing regardless of what the test asserted. Switched to a normal `from fuzz import replay_corpus` import (behavior unchanged, confirmed by the existing tests still passing); mutants went from 29/35 "not checked" to real killed/survived numbers. - tests_py/test_doctor_mcp.py's launcher smoke test needs scripts/ on disk; mutation_scope_run.sh (scratchpad tooling, not shipped) now adds it to also_copy so the scoped run's baseline-stats phase doesn't fail before scoring a single mutant (same class of gap as issue #293). Real surviving mutants found and killed with negative-case tests: - harness_consume (fuzz/replay_corpus.py): missing-consume() and module-preregistration paths were untested; added two tests pinning the exact RuntimeError message and the sys.modules[spec.name] ordering the function's own docstring documents as load-bearing. - replay() (same file, touched by this diff's call-site update): its AssertionError-wrapping path had no test that made consume() itself raise; added one. Remaining survivors in backpressure_pipeline_run (6) and exp_result_verdict (2) are the SAME equivalent mutants their source comments already document (thread `name=` kwargs; `cmp.get(op, False)`'s falsy default) -- verified by reading the actual mutant diffs, not re-litigated. Also fixes a real bug the sweep surfaced: benchmarks/lib/__init__.py eagerly imported BenchmarkDB (-> psycopg/psycopg_pool/pgvector) at package-init time, so importing the Postgres-independent benchmarks.lib.verification_report (one of the 26 extracted files) on a SQLite-only install raised ModuleNotFoundError before a single mutant could run. Every other consumer in benchmarks/lib/ already defers this import inside a function for exactly this reason; __init__.py did not follow its own package's convention. Fixed via a PEP 562 module __getattr__, with a subprocess-isolated regression test poisoning sys.modules for psycopg/psycopg_pool/pgvector. Full suite: 6864 passed, 5 skipped (was 6706 passed before this commit). Closes #282
…s are absent
test_benchmark_db_resolves_with_psycopg_present asserted the real
BenchmarkDB resolves via benchmarks.lib's lazy __getattr__, but assumed
psycopg is always importable in the test environment. CI's "Test
(SQLite backend)" job installs without the Postgres extras, so this
sanity check would fail there for the same reason BenchmarkDB is
lazy in the first place. Guarded with pytest.importorskip("psycopg")
-- the negative case (no psycopg -> loud ImportError, not silent) is
already pinned by test_benchmark_db_still_reachable_lazily_and_fails_
loudly_without_psycopg above, so this only skips the redundant
positive-case check on an install that cannot exercise it.
Full suite: 6868 passed, 5 skipped, 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #282
What
mutmutcategorically skips the body of any@dataclass-decoratedClassDef(mutmut/mutation/file_mutation.py:236). Issues #262/#280/#283established the fix — extract methods to free functions, keep the
dataclass pure data — for
ConstraintSet,RepoBadge, andRanking.This PR finishes the systemic sweep issue #282 found: all 30 remaining
@dataclass-with-methods classes acrossmcp_server/,fuzz/, andbenchmarks/lib/(26 source files), each method extracted to a freefunction taking the dataclass as its first argument, call sites updated,
existing behavior preserved.
A whole-repo AST sweep for
@dataclass-decorated classes carryingnon-dunder methods now returns zero hits (verified below).
Also fixed one real regression risk found while sweeping: a
getattr(goal, "is_active", False)call site inrecall_pipeline.pysilently defaulted to
FalseonceGoalVector.is_activebecame the freefunction
goal_vector_is_active, which would have disabled A3goal-maintenance recall re-ranking with zero signal.
grepfor.attrpatterns alone does not catch
getattr()defaults — caught by re-readingevery call site of every extracted method, not just the extraction itself.
Two more issues surfaced while re-verifying the mutation numbers below and
are fixed in this PR rather than left for a follow-up:
tests_py/fuzz/test_corpus_replay.pyimportedreplay_corpus.pyviaimportlib.util.spec_from_file_locationunder the bare name"replay_corpus", not the package-qualified"fuzz.replay_corpus"mutmut's trampoline dispatch keys mutants by (
mutmut runreported:"tests recorded trampoline hits but none match any mutant key"). Every
mutant in that file was therefore invisible to mutation testing
regardless of what the test asserted — the first table below originally
(and wrongly) reported this file at 35/35/0; the real, re-verified
number is 35/33/2 (below). Fixed via a normal
from fuzz import replay_corpusimport (behavior unchanged) plus two new negative-casetests for
harness_consume's error paths and one forreplay()'sexception-wrapping (also touched by this diff's call-site update).
benchmarks/lib/__init__.pyeagerly importedBenchmarkDB(→psycopg/psycopg_pool/pgvector) at package-init time, so importingthe Postgres-independent
benchmarks.lib.verification_report(one ofthe 26 extracted files) on a SQLite-only install raised
ModuleNotFoundErrorbefore mutmut could run a single mutant againstit. Every other consumer of
BenchmarkDBin this package alreadydefers that import inside a function for exactly this reason;
__init__.pydid not follow its own package's convention. Fixed via aPEP 562 module
__getattr__, with a subprocess-isolated regressiontest (
tests_py/benchmarks/test_lib_init_no_psycopg.py) that poisonssys.modulesforpsycopg/psycopg_pool/pgvector.Mutation evidence (§12) — per-file, per-function, scoped runs
Per the issue's methodology note (a single combined multi-file mutmut run
gives unreliable "survived" verdicts under fork()+Pool() resource
contention — reproduced and discarded), every file below was run in
isolation via a scoped driver equivalent to
scripts/mutation_check.sh(temporarily repoints
pyproject.toml's[tool.mutmut]only_mutate/source_paths/pytest_add_cli_args_test_selectionat the one file +its test(s), runs
mutmut run, restorespyproject.tomlafter). Twofiles' isolated runs were themselves unreliable for reasons unrelated to
the combined-run bug —
doctor_mcp.py's baseline-stats phase failedoutright (
scripts/launcher.pymissing from the mutants/ sandbox; fixedby adding
scriptsto the scoped run'salso_copy) andreplay_corpus.py's mutants were entirely uncounted (the module-qualification mismatch described above) — both re-run clean after their
respective fixes; the table below reflects the corrected, re-verified
counts. 677 mutants generated across the 30 classes' extracted functions:
mcp_server/core/ablation.py(AblationConfig)benchmarks/lib/verification_report.py(ExpResult)exp_result_verdict:cmp.get(op, False)vscmp.get(op, None)/no-default; both are falsy, truthiness-identical (comment at use site)fuzz/replay_corpus.py(Harness)harness_consume:spec is None or spec.loader is Noneguard is structurally unreachable for a.py-suffixedmodule_path(spec_from_file_locationonly returnsspec=None/loader-less for an unrecognized extension, verified empirically); comment at use sitemcp_server/core/attentional_control.py(AttentionAllocation)mcp_server/core/conflict_monitor.py(ConflictAssessment)mcp_server/core/context_assembly/budget.py(AssemblyMetrics)mcp_server/core/document_model.py(DocumentSection, ParsedDocument, DocumentProvenance)mcp_server/core/dual_process_retrieval.py(FamiliaritySignal)mcp_server/core/forward_model.py(ForwardModelState)mcp_server/core/goal_maintenance.py(GoalVector)mcp_server/core/habituation.py(HabituationOutcome)mcp_server/core/procedural_memory.py(ActionStep, ProceduralSkill)procedural_skill_length=len(skill.sequence), 0 mutants — same trivial-body reason)mcp_server/core/sensory_buffer.py(BufferItem)mcp_server/core/source_monitoring.py(SourceJudgement)mcp_server/core/streaming/adaptive_controller.py(AdaptiveBatchController)adaptive_batch_controller_batch_size=return controller._b, 0 mutants)mcp_server/core/streaming/backpressure_pipeline.py(BackpressurePipeline)backpressure_pipeline_run:threading.Thread(name=...)swaps; thread name is a debug label only, no test can observe it (comment at use site)mcp_server/core/value_learning.py(ValueUpdate)mcp_server/core/wiki_axis_registry.py(AxisRegistry)mcp_server/core/wiki_coverage.py(DomainCoverage, FileCoverage)mcp_server/core/wiki_groomer.py(PageAudit)mcp_server/core/wiki_redirect.py(Redirect)mcp_server/core/wiki_schema_loader.py(WikiRegistry)mcp_server/core/wiki_view_executor.py(CompiledView)mcp_server/doctor_mcp.py(McpReport)mcp_server/handlers/consolidation/cycle_types.py(CycleBudget — moved here fromheadless_authoring.pyby issue #276's mid-sweep split)mcp_server/shared/wiki_classification.py(Classification)0 unexplained survivors. 4 functions generate 0 mutants (no mutable
comparison/arithmetic/literal AST node in a pure pass-through body) —
this is a
mutmutoperator-coverage limit, not a test gap; extractionstill moves them out of the
@dataclass-skip blind spot for when afuture change adds real logic there.
replay()andmain()(both pre-existing free functions infuzz/replay_corpus.py, not part of theHarnessdataclass extraction,but touched by this diff's call-site update from
harness.consume()/harness.inputs()to the new free functions) were swept for survivors asa matter of course:
replay()'s exception-wrapping path had one real gap(no test made
consume()itself raise), fixed with a new negative-casetest;
main()'s 39 mutants are outside this diff's blast radius (itsbody is unchanged, and it is already excluded from the pytest test
selection — a pre-existing CLI-only gap, not introduced or worsened
here).
Repo-wide dataclass-with-methods sweep (issue's own repro command)
Ran the issue's own reproduction script verbatim against the final tree:
Test plan
pytest -q→ 6868 passed, 5 skipped, 116 subtestspassed, 0 failed (confirmed after the mutation re-verification and
the two additional fixes above — mutation runs restore
pyproject.tomland never touch source).ruff check+ruff format --checkon all 77 touched files: clean.scripts/check_doc_claims.py: doc claims OK.scripts/generate_repo_badges.py --check: badges OK (4 checked) — nosuite-size count reintroduced into prose/JSON docs per issue fix: eliminate the six-file test-count conflict class with a monotone floor (issue #293) #294.
.bestpractices.jsonparses.§4 size caps — measured, not asserted
Every newly extracted function is within the 40-line cap (largest:
backpressure_pipeline_runat 33 lines). This PR does not claim arepo-wide §4.1/§4.2 pass: several touched files (
auto_curator.py,recall_pipeline.py,doctor_mcp.py,procedural_memory.py, and others)were already over the 300-line file cap on
origin/mainbefore this diff(confirmed via
git show origin/main:<file> | wc -lagainst eachflagged file, re-verified with a script diffing origin/main line counts
against the current tree) and contain pre-existing over-40-line functions
unrelated to the extracted methods (e.g.
mine_skills,assess_conflict,build_clusters) — untouched debt outside this diff's blast radius(§14.3), not introduced or worsened here.
Two categories of genuine (small) growth, both within §10's explicit
Medium-stakes "≤20% flexibility on limits" band for rule 4:
mcp_server/core/wiki_redirect.pycrossed the file cap it waspreviously exactly at: 300 → 307 lines (+2.3%), from the extraction's
rationale docstring (the same pattern used for every other class in
this sweep) landing on a file that had zero headroom. Not split
further — a dedicated file-size refactor of this module is outside
this issue's scope (§15.1) and would mix an enabling refactor with this
PR's mechanical extraction.
from mechanical call-site adaptation (
x.method()→helper(x)sometimes needing an extra wrapped line):
match_skills(+10),conflict_monitor_rerank(+2),apply_habituation(+2),run_distill_drain_cycle(+4),curate_wiki.py::handler(+2), and twotest functions in
test_headless_authoring_throttle.py/test_staging_resolve_sink.py(+2/+4). None introduce new logic; allverified against
origin/mainline counts per function.Completion Ledger (§13.2)
tests_py/benchmarks/test_verification_report.py(new)ExpResultfree functionstests_py/core/test_sensory_buffer_dataclass.py(new)BufferItemfree functionstests_py/core/test_wiki_schema_loader_registry.py(new)WikiRegistryfree functionsrecall_pipeline.py::goal_maintenance_rerankgetattr→goal_vector_is_activefixtests_py/core/test_goal_maintenance_wiring.pytests_py/fuzz/test_corpus_replay.pymodule-qualified import fixtest_the_replayer_is_present,test_at_least_one_harness_exists,test_harness_holds_on_its_whole_corpus(unchanged behavior)harness_module_path/harness_corpus_path/harness_inputs/harness_consumenow scored (35 total) instead of 100% "not checked"harness_consume's missing-consume()and module-preregistration pathstest_harness_consume_missing_consume_raises_exact_message(new),test_harness_consume_preregisters_module_before_exec(new)RuntimeErrormessage assertion;sys.modules[__name__] is not Noneassertion inside a synthetic harnessreplay()'s exception-wrapping (touched by theharness_consume/harness_inputscall-site update)test_replay_wraps_consume_failure_with_input_context(new)AssertionErrormessage +__cause__assertionbenchmarks/lib/__init__.pylazyBenchmarkDBre-export (PEP 562__getattr__)tests_py/benchmarks/test_lib_init_no_psycopg.py(new)psycopg/psycopg_pool/pgvectorpoisoned insys.modules; importsbenchmarks.lib.verification_reportsuccessfullyauto_curator.py,document_normalizer.py,wiki_classifier.py,wiki_kind_detection.py,wiki_sync.py,write_gate.py,streaming/__init__.py,streaming/adaptive_writer.py, handler modules)Boy-scout (§14)
Defects seen and fixed in this PR (all in material this diff touches or
that this diff's own mutation-testing verification step executed
against):
getattr(goal, "is_active", False)stale-attribute regression inrecall_pipeline.py(found while re-reading call sites of everyextracted method).
tests_py/fuzz/test_corpus_replay.py's file-path import defeatingmutmut's coverage tracking for the entire
Harnessextraction (foundwhile producing the mutation-evidence table).
benchmarks/lib/__init__.pyeagerly importingpsycopgatpackage-init, breaking SQLite-only installs of one of the 26 extracted
files (found the same way).
test_benchmark_db_resolves_with_psycopg_present(added for defect 3)itself assumed psycopg is always installed; CI's "Test (SQLite
backend)" job installs without the Postgres extras and failed with
exactly the
ModuleNotFoundErrorthe code under test is supposed toraise loudly — the test's premise, not the code, was wrong. Guarded
with
pytest.importorskip("psycopg")(caught by this PR's own CI run,fixed same-day before merge).
No other seen-and-unfixed defects; no bypass used (no skip flags, no
narrowed globs, no temp-dir dodges — each defect above was fixed at its
source and re-verified).
Scope note (§15)
The issue's own acceptance criteria suggested sequencing this as several
PRs (one per module family) given the size. Per the release-blocker
directive for this issue queue (no new issues, adjacent PRs only where
genuinely necessary), this PR completes the full 30-class sweep in one
diff rather than splitting further — the whole-repo AST sweep confirms
there is no remainder.